JAXenter: Hallo Rafael! Auf GitHub bezeichnest du dich als “Software Consultant who likes static types”. Das statische Typensystem in Java scheint es dir also angetan zu haben – so sehr, dass du das Projekt Byteboddy gestartet hast, um Java dynamischer zu machen 😉 Was findest du am Java-Typensystem gut – was stört dich daran?
Rafael Winterhalter: Grundsätzlich empfinde ich Javas Typensystem als eines der pragmatisch besten, das ich kenne. Durch die durchgehend statische Typisierung kann man meistens sehr einfach auch durch unbekannte Java-Anwendungen navigieren, was es sehr einfach macht, sich mit einem Programm bekannt zu machen. Das hilft oft beim Onboarding neuer Entwickler, was meiner Meinung nach Java als Enterprise-Sprache erfolgreich macht.
Gleichzeitig ist Javas Typensystem einfach zu lesen, gerade weil Javacode teils ein bisschen weitschweifiger ist als andere Sprachen. Diese Explizität macht Java-Programme nach meiner Erfahrung oft sehr ausdrucksstark.
Kritik kann man vielleicht an der mangelnden Flexibiliät des Typsystems üben. Gerade generische Typen könnten natürlich in einer erweiterten Form umfangreichere Typensicherheit zulassen. Aber diese zusätzlichen Mittel würden mit einem hohen Preis kommen, gerade für beginnende Javaentwickler, denen viel Code nicht mehr zugänglich wäre.
Making Java more dynamic
JAXenter: Auf der JAX hältst du eine Session zum Thema „Making Java more dynamic.“ Darin bringst du die Möglichkeit der Code-Generierung zur Laufzeit ins Spiel. Wie genau funktioniert das?
Rafael Winterhalter: Die Java Virtual Machine führt ja eigentlich keine Java-Programme aus, sondern verarbeitet sogenannten Java Bytecode. Dieser ist ein wenig technischer aufgebaut als die Sprache Java, ist in seiner Struktur aber relativ einfach. Byte Buddy generiert dabei zur Laufzeit Java-Programme mittels einer API statt einen Compiler anzubieten. Diese Java-Klassen werden dann ins laufende Programm geladen, um dessen Verhalten zu verändern.
Dies ist manchmal notwendig, um zum Beispiel Proxies zu generieren. Solche lassen sich mit dem Compiler nur schwer erzeugen, unter anderem wegen Javas Typsystem. Weiterhin ist es möglich, existierenden Code mittels sogenannter Java-Agenten umzuschreiben. Das ermöglicht es, Anwendungen zu erweitern, ohne diese neu zu kompilieren. Dieses Verfahren nutzen beispielsweise APM-Tools, um Metriken zur Laufzeit zu sammeln.
Ein “Hello World!” mit dem Byte Buddy
Ein Hello-World
-Beispiel in Byte Buddy sieht folgendermaßen aus. Jede erzeugte Java-Klasse beginnt mit einer Instanz der ByteBuddy
-Klasse, die eine Konfiguration für die Erstellung neuer Typen repräsentiert:
1. Class<<?> dynamicType = new ByteBuddy() 2. .subclass(Object.class) 3. .method(ElementMatchers.named("toString")) 4. .intercept(FixedValue.value("Hello World!")) 5. .make() 6. .load(getClass().getClassLoader()) 7. .getLoaded(); 8. 9. assertThat(dynamicType.newInstance().toString(), is("Hello World!"));
Die Default-ByteBuddy
-Konfiguration, die im oberen Beispiel genutzt wird, erzeugt eine Java-Klasse in der neuesten Version des Klassen-File-Formats, das von der ausführenden Java Virtual Machine verstanden wird. Wie im Beispielcode ersichtlich, erweitert der erzeugte Type die Object
-Klasse und überschreibt ihre toString
-Methode, die nun den neuen Wert Hello Word!
zurückgeben sollte. Die zu überschreibende Methode wird durch einen sogenannten ElementMatcher
identifiziert.
Im Beispiel kommt ein vordefinierter Element-Matcher named(String)
zum Einsatz, der Methoden über ihre genauen Namen identifiziert. Byte Buddy bietet zahlreiche vordefinierten und gut getesteten Matcher, die in der ElementMatchers
-Klasse gesammelt vorliegen und einfach miteinander kombiniert werden können. Die Erstellung eigener Matcher ist aber ebenso möglich, indem man einfach das (funktionale) ElementMatcher
Interface implementiert.
Zur Implementierung der toString
-Methode definiert die FixedValue
-Klasse einen konstanten Rückgabe-Wert für die überschriebene Methode. Die Definition eines konstanten Wertes ist indes nur ein Beispiel für die verschiedenen Methoden-Interceptors, die Byte Buddy mitbringt. Durch die Implementierung des Implementation
Interface kann eine Methode auch durch eigenen Bytecode definiert werden.
Schließlich wird die beschriebene Java-Klasse erzeugt und danach in die Java Virtual Machine geladen. Zu diesem Zweck ist ein Ziel-Class-Loader nötig, der von der umgebenden Klasse abgeleitet wird. Zu guter Letzt können wir uns das Ergebnis anschauen, indem wir die toString
-Methode auf einer Instanz der erzeugten Klasse aufrufen und den Rückgabewert suchen, der den konstanten Wert aufweisen sollte, den wir erwarten.
Byte Buddy ist natürlich zu weitaus komplexeren Klassen-Generierungen fähig. Zudem ist Byte Buddy nicht auf die Erzeugung von Unterklassen begrenzt, sondern kann auch existierenden Code transformieren. Über ein API können sogenannte Java Agents definiert werden, die Code-Transformierungen zur Laufzeit einer beliebigen Java-Anwendung ermöglichen.
JAXenter: Kannst du einmal ein Beispiel nennen, wie durch Code-Generierung zur Laufzeit besser modularisierte Anwendungen möglich werden?
Rafael Winterhalter: Beispiele bieten sich in jeder Enterprise-Anwendung, die Code-Generierung nutzt. So implementiert etwa Spring seine AOP-Aspekte mit Hilfe von Klassen, die zur Laufzeit generiert werden. Ist eine Methode beispielsweise als transaktional markiert, so erstellt Spring eine Subklasse, die diese Logik in einer überschriebenen Methode bereitstellt. Ohne eine solche Möglichkeit könnte Spring diese Funktion nur anbieten, wenn ein Nutzer explizit eine Methode in Spring aufruft, was diesen Code stärker an Spring bindet.
JAXenter: Wie erwähnt hast du mit dem Byte-Buddy-Projekt selbst eine Code-Generierungs-Library am Start. Wie unterscheidet sich Byte Buddy zu anderen Libraries wie ASM, Javassist oder cglib?
Rafael Winterhalter: Viele Bibliotheken zur Code-Generierung erfordern ein gewisses Verständniss von Java Bytecode und lassen viele Fehler zu, wenn die APIs nicht richtig bedient werden. Das gilt besonders für ASM. Byte Buddy hat das Ziel, erfahrenen Java-Programmiereren das Arbeiten mit Bytecode leicht zu machen, indem es sich an die Syntax von Java-Programmen anlehnt. Als Entwickler soll man nicht über Bytecode nachdenken müssen und Byte Buddy nur beschreiben, welchen Code man generieren möchte.
JAXenter: Welche Änderungen am Java-Typensystem bzw. der JVM würdest du dir für zukünftige Java-Versionen wünschen?
Rafael Winterhalter: So wenige wie möglich. Javas Typensystem ist bereits sehr ausdrucksstark, und auch weniger erfahrene Entwickler werden schnell produktiv. Ich hoffe, dass diese Anforderung nicht nach oben verschoben wird. Natürlich machen die neuen funktionalen Züge von Java gewisse Änderungen im Typensystem notwendig, aber insgesamt hoffe ich, dass das VM Team seinen konservativen Kurs beibehält.
JAX 2017 – CORE JAVA & JVM LANGUAGES